/***********************************************************************
 * Copyright (c) 2013-2025 General Atomics Integrated Intelligence, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License, Version 2.0
 * which accompanies this distribution and is available at
 * https://www.apache.org/licenses/LICENSE-2.0
 ***********************************************************************/


package org.locationtech.geomesa.utils.geohash

import org.locationtech.jts.geom._

/**
 *   A bounding box is a lat-lon rectangle defined by a "lower left"
 *    and an "upper right" point.
 *
 *   We do make the assumption that the lower left corner's latitude and longitude are less
 *   than or equal to the upper right corner's.
 *
 *   NB: Lat-lon rectangles which cross the {+|-}180 longitude line cannot be represented.
 */
case class BoundingBox(ll: Point, ur: Point) {
  
  import org.locationtech.geomesa.utils.geohash.BoundingBox._

  require(ll.getX <= ur.getX, s"Bounding box lower left X: ${ll.getX} > upper right X: ${ur.getX}")
  require(ll.getY <= ur.getY, s"Bounding box lower left Y: ${ll.getY} > upper right Y: ${ur.getY}")

  lazy val envelope: Envelope = new Envelope(ll.getX, ur.getX, ll.getY, ur.getY)
  lazy val geom: Geometry = latLonGeoFactory.toGeometry(envelope)

  lazy val ul: Point = latLonGeoFactory.createPoint(new Coordinate(ll.getX, ur.getY))
  lazy val lr: Point = latLonGeoFactory.createPoint(new Coordinate(ur.getX, ll.getY))

  def intersects(bbox: BoundingBox): Boolean = covers(bbox.ll) || covers(bbox.ul) ||
                                               covers(bbox.lr) || covers(bbox.ur)

  /**
   * This bounding box covers bbox iff no points of bbox lie in the exterior of this bounding box.
   * @param bbox
   * @return
   */
  def covers(bbox: BoundingBox):Boolean = covers(bbox.ll) && covers(bbox.ul) &&
                                          covers(bbox.lr) && covers(bbox.ur)

  /**
   * This bounding box covers pt iff pt does not line in the exterior of this bounding box.
   * @param pt
   * @return
   */
  def covers(pt: Point): Boolean = (ll.getX <= pt.getX && pt.getX <= ur.getX) &&
                                   (ll.getY <= pt.getY && pt.getY <= ur.getY)

  /**
   * This bounding box contains geom iff no points of goem lie in the exterior of this bounding box,
   * and at least one point of the interior of goem lies in the interior of this bounding box.
   * @param otherGeom
   * @return
   */
  def contains(otherGeom: Geometry): Boolean = geom.contains(otherGeom)

  lazy val minLon: Double = ll.getX
  lazy val minLat: Double = ll.getY
  lazy val maxLon: Double = ur.getX
  lazy val maxLat: Double = ur.getY

  // these very simple calculations work because this class assumes min{x|y} < max{x|y}
  // (no date line or pole wrap-arounds)
  lazy val midLon: Double = (minLon + maxLon) / 2
  lazy val midLat: Double = (minLat + maxLat) / 2
}

object BoundingBox {

  lazy val latLonGeoFactory = new GeometryFactory(new PrecisionModel(PrecisionModel.FLOATING), 4326)

  def apply(x1: Double, x2: Double, y1: Double, y2: Double): BoundingBox =
     apply(Bounds(Math.min(x1, x2), Math.max(x1, x2)),
           Bounds(Math.min(y1, y2), Math.max(y1, y2)))

  def apply(lons: Bounds, lats: Bounds): BoundingBox = {
    val Bounds(minLat, maxLat) = lats
    val Bounds(minLon, maxLon) = lons
    new BoundingBox(
      latLonGeoFactory.createPoint(new Coordinate(minLon, minLat)),
      latLonGeoFactory.createPoint(new Coordinate(maxLon, maxLat)))
  }

  def apply(env: Envelope): BoundingBox = {
    apply(env.getMinX, env.getMaxX, env.getMinY, env.getMaxY)
  }

  def intersects(l: BoundingBox, r: BoundingBox): Boolean = l.geom.intersects(r.geom)

  /**
   * get geohash that covers the bounding box to the given precision 
   * @param bbox
   * @param precision
   * @return
   */
  def getCoveringGeoHash(bbox: BoundingBox, precision: Int): GeoHash = {
    val ll = GeoHash(bbox.ll, precision).hash
    val ur = GeoHash(bbox.ur.getX-1e-12, bbox.ur.getY-1e-12, precision).hash
    GeoHash(ll.zip(ur).takeWhile(Function.tupled(_ == _)).map(_._1).mkString)
  }
}
